import ts from 'typescript';
import openapiTS, { type OpenAPI3, astToString } from 'openapi-typescript';
import { Effect } from 'effect';
import { TaggedError } from 'effect/Data';

export class GenerateTypeFromJsonSchemaError extends TaggedError('json-parsing-error')<{
  cause?: unknown;
  message?: string;
}> {}

export function generateTypeFromJsonSchema(
  name: string,
  schema: { readonly [x: string]: unknown }
) {
  const openApi3Definition = {
    openapi: '3.1.0',
    components: {
      schemas: {
        [name]: {
          type: 'object',
          properties: schema['properties'],
        },
      },
    },
  } as unknown as OpenAPI3;

  return Effect.gen(function* () {
    const astNodes = yield* Effect.tryPromise({
      try: () => openapiTS(openApi3Definition),
      catch: e =>
        new GenerateTypeFromJsonSchemaError({
          cause: e,
          message: 'Failed to generate type from JSON schema',
        }),
    });

    const components = astNodes.find(isInterfaceComponents);

    if (components === undefined) {
      return yield* Effect.fail(
        new GenerateTypeFromJsonSchemaError({
          message:
            'Failed to find `components` interface in types generated by `openapi-typescript`',
        })
      );
    }

    // This is the TypeScript definition obtained via `openapi-typescript`, which contains the types we want + some garbage we don't need.
    // We thus have to extract the type we want using the TypeScript API.
    const sourceCode = astToString(components);

    const sourceFile = ts.createSourceFile('virtual.ts', sourceCode, ts.ScriptTarget.Latest, true);

    // Navigate to components.schemas[name]
    const schemasProp = components.members.find(
      m => ts.isPropertySignature(m) && (m.name as ts.Identifier).text === 'schemas'
    ) as ts.PropertySignature | undefined;

    if (!schemasProp || !schemasProp.type || !ts.isTypeLiteralNode(schemasProp.type)) {
      return yield* Effect.fail(
        new GenerateTypeFromJsonSchemaError({
          message: '`schemas` is not a type literal',
        })
      );
    }

    const schemasTypeLiteral = schemasProp.type;
    const payloadProp = schemasTypeLiteral.members.find(
      m => ts.isPropertySignature(m) && (m.name as ts.Identifier).text === name
    ) as ts.PropertySignature | undefined;

    if (!payloadProp || !payloadProp.type) {
      return yield* Effect.fail(
        new GenerateTypeFromJsonSchemaError({
          message: `Failed to find type for ${name}`,
        })
      );
    }

    let typeNode: ts.TypeNode;

    // Handle different type node cases
    if (ts.isTypeLiteralNode(payloadProp.type)) {
      // Case 1: Type literal with specific properties (e.g., { foo: string; bar: number; })
      // BUT also handle empty type literals (which represent generic objects with no properties)
      if (payloadProp.type.members.length === 0) {
        // Empty type literal {} should be converted to 'object'
        typeNode = ts.factory.createTypeReferenceNode('object');
      } else {
        // Type literal with specific properties
        typeNode = payloadProp.type;
      }
    } else if (
      ts.isTypeReferenceNode(payloadProp.type) &&
      payloadProp.type.typeName &&
      ts.isIdentifier(payloadProp.type.typeName)
    ) {
      const typeNameText = payloadProp.type.typeName.text;
      if (typeNameText === 'object') {
        // Case 2a: Generic 'object' type reference - output TypeScript 'object' type
        typeNode = ts.factory.createTypeReferenceNode('object');
      } else if (typeNameText === 'Record') {
        // Case 2b: Record type (generated by openapi-typescript for generic objects) - convert to 'object'
        typeNode = ts.factory.createTypeReferenceNode('object');
      } else {
        // Case 2c: Other type references - fall back to 'unknown'
        typeNode = ts.factory.createTypeReferenceNode('unknown');
      }
    } else if (
      ts.isToken(payloadProp.type) &&
      payloadProp.type.kind === ts.SyntaxKind.ObjectKeyword
    ) {
      // Case 3: Direct 'object' keyword - output TypeScript 'object' type
      typeNode = ts.factory.createTypeReferenceNode('object');
    } else {
      // Case 4: Other unexpected types - fall back to 'unknown'
      typeNode = ts.factory.createTypeReferenceNode('unknown');
    }

    // Print the type as a string with JSDoc preserved
    const printer = ts.createPrinter({ removeComments: false });

    const printedType = ts.factory.createTypeAliasDeclaration(
      [],
      // [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
      name,
      undefined,
      typeNode
    );

    const printedText = printer.printNode(ts.EmitHint.Unspecified, printedType, sourceFile);

    // Formats the type so that it is indented by 2 spaces instead of 4
    const adjustedText = printedText.replace(/^ {4}/gm, '  ');

    return adjustedText;
  });
}

function isInterfaceComponents(node: ts.Node): node is ts.InterfaceDeclaration {
  return ts.isInterfaceDeclaration(node) && node.name.escapedText === 'components';
}
